/*
 Copyright (C) 2009 QDSS.org
 
 This program is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 the Free Software Foundation, either version 3 of the License, or
 any later version.

 This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU General Public License for more details.
 
 You should have received a copy of the GNU General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.qdss.topisv.api;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Random;
import java.util.zip.GZIPInputStream;

import org.apache.commons.httpclient.ConnectionPoolTimeoutException;
import org.apache.commons.httpclient.Header;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpMethod;
import org.apache.commons.httpclient.HttpMethodBase;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager;
import org.apache.commons.httpclient.URIException;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.params.HttpClientParams;
import org.apache.commons.httpclient.params.HttpConnectionManagerParams;
import org.apache.commons.httpclient.params.HttpMethodParams;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.qdss.topisv.api.response.XmlResultImpl;

/**
 * Http调用
 * 
 * @author <a href="mailto:zhaofang123@gmail.com">FANGFANG ZHAO</a>
 * @since 0.1.0, Nov 17, 2009
 * @version $Id: HttpClientFetcher.java 75 2013-09-07 11:26:54Z zhaofang123 $
 */
public class HttpClientFetcher {

	private static final Log LOG = LogFactory.getLog(HttpClientFetcher.class);

	/**
	 * 最大连接数
	 */
	public static final int MAX_CONNECTIONS_NUMBER = 512;
	/**
	 * 获取连接超时
	 */
	public static final int CONNECTION_GET_TIMEOUT = 60 * 1000;
	/**
	 * 从服务器读取数据超时(Socket)
	 */
	public static final int READ_DATA_TIMEOUT = 60 * 1000;

	/**
	 * 调用超时记录日志时间 3秒
	 */
	public static final int TIMEOUT_LOGGING = 3 * 1000;
	/**
	 * 尝试最大获取连接次数
	 */
	public static final int CONNECTION_GET_RETRY = 3;
	/**
	 * 尝试最大读取数据次数
	 */
	public static final int READ_DATA_RETRY = 3;
	
	private int timeoutLogging;
	private int maxConnectGetRetry;
	private int maxReadRetry;
	private boolean gzip;
	
	final private HttpClient httpClient;
	
	/**
	 */
	public HttpClientFetcher() {
		this(TIMEOUT_LOGGING, CONNECTION_GET_RETRY, READ_DATA_RETRY, false);
	}

	/**
	 * @param timeoutLogging
	 * @param maxConnectGetRetry
	 * @param maxReadRetry
	 * @param gzip
	 */
	public HttpClientFetcher(int timeoutLogging, int maxConnectGetRetry, int maxReadRetry, boolean gzip) {
		this.timeoutLogging = timeoutLogging;
		this.maxConnectGetRetry = maxConnectGetRetry;
		this.maxReadRetry = maxReadRetry;
		this.gzip = gzip;
		
		LOG.info("Initializing HttpClient ...." +
				"{ TIMEOUT_LOGGING: " + timeoutLogging + 
				", MAX_CONNECT_GETRETRY: " + maxConnectGetRetry + 
				", MAX_READRETRY: " + maxReadRetry + 
				", MAX_CONNECTIONS: " + MAX_CONNECTIONS_NUMBER +
				", GZIP: " + gzip + " }");

		HttpClientParams clientParams = new HttpClientParams();
		clientParams.setConnectionManagerTimeout(CONNECTION_GET_TIMEOUT);
		clientParams.setSoTimeout(READ_DATA_TIMEOUT);
		clientParams.setParameter(HttpMethodParams.HTTP_CONTENT_CHARSET, ApiUtils.ENCODING_DEFAULT);
		clientParams.setConnectionManagerClass(MultiThreadedHttpConnectionManager.class);
		httpClient = new HttpClient(clientParams);
		
		HttpConnectionManagerParams connectionParams = httpClient.getHttpConnectionManager().getParams();
		connectionParams.setDefaultMaxConnectionsPerHost(MAX_CONNECTIONS_NUMBER);
		connectionParams.setMaxTotalConnections(MAX_CONNECTIONS_NUMBER);
	}
	
	/**
	 * @return
	 */
	public HttpClient getHttpClient() {
		return httpClient;
	}
	
	/**
	 * @return
	 */
	public int getTimeoutLogging() {
		return timeoutLogging;
	}
	
	/**
	 * @param timeoutLogging
	 */
	public void setTimeoutLogging(int timeoutLogging) {
		this.timeoutLogging = timeoutLogging;
	}
	
	/**
	 * @return
	 */
	public int getMaxConnectGetRetry() {
		return maxConnectGetRetry;
	}
	
	/**
	 * @param maxConnectGetRetry
	 */
	public void setMaxConnectGetRetry(int maxConnectGetRetry) {
		this.maxConnectGetRetry = maxConnectGetRetry;
	}
	
	/**
	 * @return
	 */
	public int getMaxReadRetry() {
		return maxReadRetry;
	}
	
	/**
	 * @param maxReadRetry
	 */
	public void setMaxReadRetry(int maxReadRetry) {
		this.maxReadRetry = maxReadRetry;
	}
	
	/**
	 * @return
	 */
	public boolean isGzip() {
		return gzip;
	}
	
	/**
	 * @param gzip
	 */
	public void setGzip(boolean gzip) {
		this.gzip = gzip;
	}
	
	/**
	 * @param method
	 * @return
	 */
	public Result executeMethod(HttpMethod method) {
		return executeMethod(method, 1, 1, 1);
	}
	
	/**
	 * @param method
	 * @param connectGetRetry
	 * @param readRetry
	 * @param errorRetry
	 * @return
	 */
	public Result executeMethod(HttpMethod method, int connectGetRetry, int readRetry, int errorRetry) {
		if (isGzip()) {
			method.addRequestHeader("Accept-Encoding", "gzip, deflate");
		}
		
		long start = System.currentTimeMillis();
		int time = 0;
		try {
			int status = httpClient.executeMethod(method);
			if (status != HttpStatus.SC_OK) {
				throw new BadHttpStatus(status, method.getURI().toString());
			}
			
			time = (int) (System.currentTimeMillis() - start);
			return parseXmlResult( method, time );
		}
		catch (ConnectionPoolTimeoutException ex) {
			sleep0(2000);
			if (connectGetRetry++ < this.maxConnectGetRetry) {
				LOG.warn("Timeout for connection get! retry: " + connectGetRetry + "/" + this.maxConnectGetRetry);
				return executeMethod(copyMethod(method), connectGetRetry, readRetry, errorRetry);
			}
			throw new ApiInvokeFailureException(ex);
		}
		catch (IOException ex) {  // SocketTimeoutException/read timeout, SocketException/socket reset
			sleep0(2000);
			if (readRetry++ < this.maxReadRetry) {
				LOG.warn("Timeout for socket read! retry: " + readRetry + "/" + this.maxReadRetry);
				return executeMethod(copyMethod(method), connectGetRetry, readRetry, errorRetry);
			}
			throw new ApiInvokeFailureException(ex);
		}
		catch (BadHttpStatus ex) {
			sleep0(3000);
			final int maxRetry = ex.getStatus() == HttpStatus.SC_NOT_FOUND ? 6 : 3;
			if (errorRetry++ < maxRetry) {
				LOG.warn("Http Status Error! retry " + errorRetry + ": " + ex.getLocalizedMessage());
				return executeMethod(copyMethod(method), connectGetRetry, readRetry, errorRetry);
			}
			throw new ApiInvokeFailureException(ex);
		}
		catch (Throwable ex) {
			if (ex instanceof ApiInvokeFailureException) {
				throw (ApiInvokeFailureException) ex;
			}
			throw new ApiInvokeFailureException("Unknow error on executed http method!", ex);
		} 
		finally {
			if (time > this.timeoutLogging) {
				try { LOG.warn("Time of invocation is " + time + " ms. URI: " + method.getURI()); } catch (URIException e) { }
			}
			method.releaseConnection();
		}
	}
	
	/**
	 * 读取二进制数据, 如图片
	 * 
	 * @param uri
	 * @return
	 */
	public byte[] readBinary(String uri) {
		return readBinary(new GetMethod(uri));
	}
	
	/**
	 * 读取二进制数据, 如图片
	 * 
	 * @param method
	 * @return
	 * 
	 * @see HttpMethodBase#getResponseBody()
	 */
	public byte[] readBinary(HttpMethod method) {
		long start = System.currentTimeMillis();
		int time = 0;
		try {
			int status = httpClient.executeMethod(method);
			if (status != HttpStatus.SC_OK) {
				throw new BadHttpStatus(status, method.getURI().toString());
			}
			
			time = (int) (System.currentTimeMillis() - start);
			return method.getResponseBody();
		}
		catch (IOException ex) {  // SocketTimeoutException/read timeout, SocketException/socket reset
			throw new ApiInvokeFailureException(ex);
		}
		catch (Throwable ex) {
			if (ex instanceof ApiInvokeFailureException) {
				throw (ApiInvokeFailureException) ex;
			}
			throw new ApiInvokeFailureException("Unknow error on executed http method!", ex);
		} 
		finally {
			if (time > this.timeoutLogging) {
				try { LOG.warn("Time of invocation is " + time + " ms. URI: " + method.getURI()); } catch (URIException e) { }
			}
			method.releaseConnection();
		}
	}
	
	// -----------------------------------------------------------------------------------
	
	/*-
	 * 复制HTTP方法
	 */
	private HttpMethod copyMethod(HttpMethod method) {
		method.releaseConnection();
		
		HttpMethod clone = null;
		if ("GET".equalsIgnoreCase(method.getName())) {
			clone = new GetMethod();
		} else {
			clone = new PostMethod();
		}
		
		try {
			clone.setURI(method.getURI());
		} catch (URIException e) { }
		return clone;
	}
	
	/*-
	 * 解析调用结果
	 */
	private Result parseXmlResult(HttpMethod method, int time) throws IOException {
		final Header[] headers = method.getResponseHeaders();
		
		String responseText = null;
		BufferedReader reader = null;
		try {
			InputStream stream = method.getResponseBodyAsStream();
			if (stream.available() > -1) {
				Header encoding = method.getResponseHeader("Content-Encoding");
				if (encoding != null && encoding.getValue().toUpperCase().contains("GZIP")) {
					stream = new GZIPInputStream(stream);
				}
				reader = new BufferedReader(new InputStreamReader(stream, ApiUtils.ENCODING_DEFAULT));
				
				StringBuffer sb = new StringBuffer();
				char[] buffer = new char[1024 * 4];
				int read = -1;
				while ((read = reader.read(buffer)) > 0) {
					char[] chunk = new char[read];
					System.arraycopy(buffer, 0, chunk, 0, read);
					sb.append(chunk);
				}
				responseText = sb.toString();
			}
		} catch (IOException ex) {
			throw ex;
		} finally {
			try {
				reader.close();
			} catch (Exception ex){}
			method.releaseConnection();
		}
		
		return (new XmlResultImpl(headers, time, responseText));
	}
	
	/*-
	 * 出错时的线程等待
	 */
	static void sleep0(int rndMs) {
		try {
			Thread.sleep(new Random().nextInt(rndMs));
		} catch (InterruptedException e) {
			Thread.currentThread().interrupt();
		}
	}
	
	/*-
	 * Error HTTP STATUS
	 */
	public static class BadHttpStatus extends ApiInvokeFailureException {
		private static final long serialVersionUID = -113341732969890690L;
		private int status;
		BadHttpStatus(int status, String url) {
			super("Bad request! Status: " + status + "(" + HttpStatus.getStatusText(status) + ")! URL: " + url);
			this.status = status;
		}
		/**
		 * 状态码
		 * 
		 * @return
		 */
		public int getStatus() {
			return status;
		}
	}
}
